using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using WebFramework.ActionResults;

namespace WebFramework
{
    public enum Priority { Low, Normal, High };

    class Route : IComparable<Route>
    {
        public static RegexOptions DefaultOptions = RegexOptions.IgnoreCase | RegexOptions.Compiled;
        public static bool FullMatch = true;

        Regex _regex;
        MethodInfo[] _methods;
        Priority _priority;

        public int CompareTo(Route other)
        {
            return other._priority.CompareTo(_priority);
        }

        public override string ToString()
        {
            Debug.Assert(_methods.Length > 0);
            return string.Format("{0} => {1}.{2} ({3} Priority)", _regex, _methods[0].DeclaringType.Name, _methods[0].Name, _priority);
        }

        private static string FormatPattern(string pattern)
        {
            return FullMatch ? string.Format("^(?:{0})$", pattern) : pattern;
        }

        public Route(string pattern, string action, Priority priority = Priority.Normal)
        {
            _priority = priority;
            _regex = new Regex(FormatPattern(pattern), DefaultOptions);

            int lastDot = action.LastIndexOf('.');

            if (lastDot == -1) throw new ArgumentException("Must be in the form 'Controller.Action'", "action");

            string controllerName = action.Substring(0, lastDot);
            string methodName = action.Substring(lastDot + 1);
            Type controllerType = Type.GetType(controllerName);

            if (controllerType == null) throw new Exception(
                string.Format("Could not find class '{0}'", controllerName));

            if (!controllerType.IsSubclassOf(typeof(Controller))) throw new Exception(
                string.Format("Class '{0}' is not a Controller", controllerName));

            _methods = controllerType
                .GetMethods(BindingFlags.Public | BindingFlags.Instance)
                .Where(m => m.Name == methodName)
                .ToArray();

            if (_methods.Length == 0) throw new Exception(
                string.Format("Could not find method '{0}' in class '{1}'", methodName, controllerName));
        }

        public Route(string pattern, MethodInfo method, Priority priority = Priority.Normal)
        {
            _priority = priority;
            _regex = new Regex(FormatPattern(pattern), DefaultOptions);
            _methods = new[] { method };
        }

        private static string GetMethodSignature(MethodInfo method)
        {
            string parameters = string.Join(", ", method.GetParameters()
                .Select(p => string.Format("{0} {1}", p.ParameterType.Name, 
                    p.IsOptional ? string.Format("[{0}]", p.Name) : p.Name)));
            return string.Format("{0} {1}({2})", method.ReturnType.Name, method.Name, parameters);
        }

        public bool IsMatch(string input)
        {
            return _regex.IsMatch(input);
        }

        public IActionResult Execute(string input)
        {
            Debug.Assert(_methods.Length > 0);

            var match = _regex.Match(input);
            if (!match.Success) throw new Exception(string.Format("Regex '{0}' does not match input '{1}'", _regex, input));

            var groupNames = _regex
                .GetGroupNames()
                .Skip(1)
                .Where(n => !Regex.IsMatch(n, @"^\d+$") && match.Groups[n].Success)
                .ToSet();

            var orderedMethods =
                from method in _methods
                let parameters = method.GetParameters()
                let parameterNames = parameters.Select(p => p.Name).ToSet()
                let commonParameterNames = parameterNames.Intersect(groupNames)
                orderby commonParameterNames.Count descending
                select new
                {
                    Method = method,
                    Parameters = parameters,
                    ParameterNames = parameterNames,
                    CommonParameterNames = commonParameterNames
                };

            foreach (var m in orderedMethods)
            {
                if (m.ParameterNames.Count > m.CommonParameterNames.Count)
                {
                    var excessParameterNames = m.ParameterNames.Subtract(m.CommonParameterNames);
                    var excessParameters = m.Parameters.Where(p => excessParameterNames.Contains(p.Name));
                    if (!excessParameters.All(parameter => parameter.IsOptional)) continue;
                }

                var parameterValues = new object[m.Parameters.Length];

                for(int i=0; i<m.Parameters.Length; ++i)
                {
                    if (m.CommonParameterNames.Contains(m.Parameters[i].Name))
                    {
                        try
                        {
                            parameterValues[i] = Convert.ChangeType(match.Groups[m.Parameters[i].Name].Value, m.Parameters[i].ParameterType);
                        }
                        catch (Exception e)
                        {
                            throw new Exception(string.Format("Could not convert '{0}' to '{1}' for argument '{2}' in method '{3}.{4}'\nException: {5}",
                                match.Groups[m.Parameters[i].Name].Value, m.Parameters[i].ParameterType.Name, m.Parameters[i].Name, m.Method.DeclaringType.Name, m.Method.Name, e.Message));
                        }
                    }
                    else
                    {
                        parameterValues[i] = m.Parameters[i].DefaultValue;
                    }
                }

                var constructor = m.Method.DeclaringType.GetConstructor(Type.EmptyTypes);
                var instance = constructor.Invoke(null);

                return (IActionResult)m.Method.Invoke(instance, parameterValues);
            }

            var sb = new StringBuilder();
            
            if (groupNames.Count == 0)
                sb.AppendFormat("Method '{0}.{1}' does not have an overload taking no arguments\n", _methods[0].DeclaringType.Name, _methods[0].Name);
            else
                sb.AppendFormat("Method '{0}.{1}' does not have an overload taking arguments '{2}'\n", _methods[0].DeclaringType.Name, _methods[0].Name, string.Join(", ", groupNames));
            
            sb.AppendLine("Candidates are:");
            
            foreach (var method in _methods)
                sb.AppendLine(string.Format("- {0}", GetMethodSignature(method)));
            
            throw new Exception(sb.ToString());
        }
    }
}